iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0
自我挑戰組

《轉職學習日記:JavaScript × Node.js × TypeScript × Docker × AWS ECS》系列 第 26

Day26 - 持續成長學習藍圖 - TypeScript(錯誤處理與結構優化)

  • 分享至 

  • xImage
  •  

昨天我完成了 Express + Prisma + TypeScript 的整合,
今天的目標是把錯誤處理「集中管理」,
並調整專案架構,讓控制器、服務、DTO 各自負責,
程式變得更乾淨、更可維護。


1️⃣ 問題現況

目前錯誤處理是分散在各個路由裡,例如:

try {
  await prisma.todo.delete({ where: { id } });
} catch {
  res.status(404).json({ error: "找不到這筆 Todo" });
}

這樣每個 API 都要重複 try/catch
如果未來錯誤邏輯要修改,就得改一堆地方。


2️⃣ 目標:集中錯誤處理

我們要做的三件事:

  1. 建立一個 自訂錯誤類別HttpException
  2. 建立一個 全域錯誤處理 Middleware
  3. Service + Controller 分層,讓程式結構更清晰

3️⃣ 新的專案結構

src/
 ├── controllers/
 │   └── todo.controller.ts
 ├── services/
 │   └── todo.service.ts
 ├── middleware/
 │   ├── error-handler.ts
 │   └── validate-dto.ts
 ├── dto/
 │   ├── create-todo.dto.ts
 │   └── update-todo.dto.ts
 ├── prisma/
 │   └── client.ts
 ├── routes/
 │   └── todo.routes.ts
 ├── utils/
 │   └── http-exception.ts
 ├── index.ts

4️⃣ 定義自訂錯誤型別

src/utils/http-exception.ts

export class HttpException extends Error {
  status: number;
  message: string;

  constructor(status: number, message: string) {
    super(message);
    this.status = status;
    this.message = message;
  }
}

這樣我就能用 throw new HttpException(404, "找不到這筆資料") 來拋出錯誤。


5️⃣ 建立全域錯誤處理 Middleware

src/middleware/error-handler.ts

import { Request, Response, NextFunction } from "express";
import { HttpException } from "../utils/http-exception";

export function errorHandler(err: unknown, req: Request, res: Response, next: NextFunction) {
  console.error("❌ 錯誤發生:", err);

  if (err instanceof HttpException) {
    return res.status(err.status).json({ error: err.message });
  }

  // 非預期錯誤
  return res.status(500).json({ error: "伺服器內部錯誤" });
}

這樣所有 throw new HttpException() 的錯誤都會被自動捕捉。


6️⃣ 實作 Service 層

src/services/todo.service.ts

import prisma from "../prisma/client";
import { HttpException } from "../utils/http-exception";

export class TodoService {
  async findAll() {
    return prisma.todo.findMany();
  }

  async create(task: string, note?: string) {
    return prisma.todo.create({ data: { task, note } });
  }

  async update(id: number, data: any) {
    const todo = await prisma.todo.findUnique({ where: { id } });
    if (!todo) throw new HttpException(404, "找不到這筆 Todo");

    return prisma.todo.update({ where: { id }, data });
  }

  async delete(id: number) {
    const todo = await prisma.todo.findUnique({ where: { id } });
    if (!todo) throw new HttpException(404, "找不到這筆 Todo");

    await prisma.todo.delete({ where: { id } });
    return { message: "Todo 已刪除" };
  }
}

Service 負責邏輯,不管輸入輸出,只專心操作資料。


7️⃣ 建立 Controller

src/controllers/todo.controller.ts

import { Request, Response, NextFunction } from "express";
import { TodoService } from "../services/todo.service";

const todoService = new TodoService();

export class TodoController {
  async getAll(req: Request, res: Response, next: NextFunction) {
    try {
      const todos = await todoService.findAll();
      res.json(todos);
    } catch (err) {
      next(err);
    }
  }

  async create(req: Request, res: Response, next: NextFunction) {
    try {
      const { task, note } = req.body;
      const todo = await todoService.create(task, note);
      res.status(201).json(todo);
    } catch (err) {
      next(err);
    }
  }

  async update(req: Request, res: Response, next: NextFunction) {
    try {
      const id = Number(req.params.id);
      const todo = await todoService.update(id, req.body);
      res.json(todo);
    } catch (err) {
      next(err);
    }
  }

  async delete(req: Request, res: Response, next: NextFunction) {
    try {
      const id = Number(req.params.id);
      const result = await todoService.delete(id);
      res.json(result);
    } catch (err) {
      next(err);
    }
  }
}

Controller 專心處理 請求與回應,不碰業務邏輯。


8️⃣ 路由改成呼叫 Controller

src/routes/todo.routes.ts

import { Router } from "express";
import { validateDto } from "../middleware/validate-dto";
import { CreateTodoDto } from "../dto/create-todo.dto";
import { UpdateTodoDto } from "../dto/update-todo.dto";
import { TodoController } from "../controllers/todo.controller";

const router = Router();
const controller = new TodoController();

router.get("/", controller.getAll.bind(controller));
router.post("/", validateDto(CreateTodoDto), controller.create.bind(controller));
router.put("/:id", validateDto(UpdateTodoDto), controller.update.bind(controller));
router.delete("/:id", controller.delete.bind(controller));

export default router;

9️⃣ 主程式整合

src/index.ts

import "reflect-metadata";
import express from "express";
import todoRoutes from "./routes/todo.routes";
import { errorHandler } from "./middleware/error-handler";

const app = express();
app.use(express.json());
app.use("/todos", todoRoutes);
app.use(errorHandler); // 全域錯誤處理要放在最後

app.listen(3000, () => {
  console.log("🚀 Server running on http://localhost:3000");
});

🔟 測試錯誤處理

✅ 正確請求

curl -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"task": "Day 26 完成錯誤處理"}'

❌ 錯誤請求

curl -X PUT http://localhost:3000/todos/999 \
  -H "Content-Type: application/json" \
  -d '{"task": "不存在的 Todo"}'

輸出:

{ "error": "找不到這筆 Todo" }

🎯 成功!錯誤被集中處理、訊息乾淨又一致。


🎯 學習心得 / 今日收穫

今天讓整個 API 架構升級成「真正可維護」的版本:

  • HttpException 讓錯誤更語意化
  • errorHandler 中央化錯誤管理
  • 分層架構:Controller 負責請求、Service 負責邏輯
  • TypeScript 幫我自動補型別,程式更安全

以前寫 Express 是「能跑就好」,
現在這樣的結構,已經接近真正的專案規模。


上一篇
Day25 - 持續成長學習藍圖 - TypeScript(Express + Prisma + TypeScript 整合)
下一篇
Day27 - 持續成長學習藍圖 - TypeScript(小作品)
系列文
《轉職學習日記:JavaScript × Node.js × TypeScript × Docker × AWS ECS》29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言